﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace MedievalGameProject
{
    public class Actor : DrawableGameComponent, IComparable
    {
        public BoundingShapes boundingShapes { get; protected set; }

        public BasicModel model { get; protected set; }
        public Vector3 position { get; protected set; }
        public Vector3 direction { get; set; }
        public Vector3 up { get; protected set; }

        // gravity stuff
        public bool usesGravity = false;
        protected float speed = 1f;
        protected float fallAccel = -0.05f;
        protected float actorHeight;
        protected Ray downRay;

        // to prevent climbing steep terrain
        protected float poleDist = 2.0f;

        // for jumping
        protected bool inAir;
        protected float jumpVelocity = 1.0f;

        protected Vector3 velocity;
        protected float startFallThreshold = 3f;

        public Actor(Game game, BasicModel m, Vector3 position, float actorHeight) : base(game)
        {
            this.model = m;
            this.position = position;
            this.actorHeight = actorHeight;

            Initialize();

            m.Move(position);

            downRay = new Ray(new Vector3(position.X, position.Y, position.Z), Vector3.Down);

            velocity = new Vector3();
        }

        public override void Initialize()
        {
            base.Initialize();

            direction = Vector3.UnitZ;
            up = Vector3.Up;
        }

        protected override void LoadContent()
        {
            base.LoadContent();

            Console.WriteLine("Creating hitbox for an actor!");

            boundingShapes = new BoundingShapes(position,
                new BoundingBox(new Vector3(-1.5f, -10, -1.5f), new Vector3(1.5f, 3, 1.5f)), GraphicsDevice);
        }

        public override void Update(GameTime gameTime)
        {
            model.Update(gameTime);
            if(usesGravity)
                ApplyGravity();
            model.SetPosition(position);
            boundingShapes.UpdatePosition(position);
        }

        public override void Draw(GameTime gameTime)
        {
            model.Draw(gameTime);
            boundingShapes.Draw(gameTime);
        }

        public virtual void ApplyGravity()
        {
            if (inAir)
            {
                velocity.Y += fallAccel;
                Move(new Vector2(velocity.X, velocity.Z) * speed);
            }

            downRay.Position = position;

            float? distFromGround = Game1.terrain.Intersects(downRay);

            if (distFromGround != null)
            {
                float yPos = position.Y - distFromGround.Value + actorHeight;

                if (distFromGround > actorHeight || velocity.Y > 0)
                {
                    if (!inAir && (distFromGround.Value - actorHeight) > startFallThreshold)
                        inAir = true;

                    if (inAir && distFromGround + velocity.Y > actorHeight)
                    {
                        yPos = position.Y + velocity.Y;
                    }
                }
                else
                {
                    if (inAir)
                    {
                        inAir = false;
                        velocity.Y = 0;
                    }
                }

                position = new Vector3(position.X, yPos, position.Z);
            }
        }

        public void Move(Vector2 directionToMove)
        {
            Move(new Vector3(directionToMove.X * speed, 0, directionToMove.Y * speed));
        }

        public virtual void Move(Vector3 directionToMove)
        {
            Vector3 moveDir = directionToMove;
            moveDir.Normalize();

            Vector3 velocity = directionToMove * speed;

            Vector3 nextPosition = position + velocity;
            Ray poleRay = new Ray(new Vector3(nextPosition.X, nextPosition.Y + 1, nextPosition.Z), moveDir);

            float? distToTerrain = Game1.terrain.Intersects(poleRay);

            if (distToTerrain != null)
            {
                if (distToTerrain.Value < poleDist)
                    return;
            }

            position += velocity;
        }

        public virtual void Jump()
        {
            if (!inAir)
            {
                velocity.Y = jumpVelocity;
                inAir = true;
            }
        }
        //Set Facing Direction only in x and z planes
        public void SetFacingDirection2D(Vector3 direction)
        {
            Vector3 prevDirection = this.direction;
            direction = new Vector3(direction.X, 0, direction.Z);
            direction.Normalize();
            this.direction = direction;
            Quaternion rot = GetRotation(prevDirection, direction, up);
            if(rot.Y != 1)
                model.worldRotation *= Matrix.CreateFromQuaternion(rot);
        }

        public void SetFacingDirection3D(Vector3 direction)
        {
            Vector3 prevDirection = this.direction;
            direction.Normalize();
            this.direction = direction;
            Quaternion rot = GetRotation(prevDirection, direction, up);
            model.worldRotation *= Matrix.CreateFromQuaternion(rot);
        }

        public static Quaternion GetRotation(Vector3 source, Vector3 dest, Vector3 up)
        {
            float dot = Vector3.Dot(source, dest);

            if (Math.Abs(dot - (-1.0f)) < 0.000001f)
            {
                // vector a and b point exactly in the opposite direction, 
                // so it is a 180 degrees turn around the up-axis
                return new Quaternion(up, MathHelper.ToRadians(180.0f));
            }
            if (Math.Abs(dot - (1.0f)) < 0.000001f)
            {
                // vector a and b point exactly in the same direction
                // so we return the identity quaternion
                return Quaternion.Identity;
            }

            float rotAngle = (float)Math.Acos(dot);
            Vector3 rotAxis = Vector3.Cross(source, dest);
            rotAxis = Vector3.Normalize(rotAxis);
            return Quaternion.CreateFromAxisAngle(rotAxis, rotAngle);
        }

        public int CompareTo(Object obj) {
            int rval = 1;
            if (obj is Actor)
            {
                float objDist = (((Actor)obj).position - Game1.camera.cameraPosition).Length();
                float thisDist = (position - Game1.camera.cameraPosition).Length();

                if (thisDist < objDist)
                    rval = -1;
            }

            return rval;
        }

        public void SetPosition(Vector3 pos)
        {
            this.position = pos;
            model.SetPosition(pos);
            boundingShapes.UpdatePosition(pos);
        }
    }
}
